package comp.comp.wxxcx_login.service;

import org.springframework.data.domain.Page;
import org.apache.commons.codec.net.URLCodec;

import java.util.*;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.alibaba.fastjson.*;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.util.Formatter;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import comp.comp.wxxcx_login.service.Utils;

@Service
public class ThirdPartyLoginUserService {
    private final static Logger logger = LoggerFactory.getLogger(ThirdPartyLoginUserService.class);

    private String wxLogin(String code, Boolean login2Uaa, String encryptedData, String iv) throws Exception {
        int statusCode = 0;
        String ret = Utils.GET(getUrl(code), false);
        logger.info("===getOpenId(" + code + ")=" + ret);
        // 向UAA中注册影子帐户，UAA会同时创建实际帐户
        // 如果注册成功，已经调用了/login生成了user_session的Cookie
        // 如果注册失败，需要用OpenId生成Cookie
        JSONObject obj = JSON.parseObject(ret);
        if (obj.getString("openid") == null)
            throw new Exception(ret);

        obj.put("appId", Utils.GetAppIdFromReferer());
        obj.put("clientType", "miniapp");
        if (encryptedData != null && encryptedData.length() > 0 && iv != null && iv.length() > 0) {
            Map<String,String> map = new HashMap<String,String>();
            map.put("X-Session-SessionKey", obj.getString("session_key"));
            ret = getUserInfoInternal(encryptedData, iv, map, false);
            logger.info("===Decrypt user info result:" + ret);
            obj.putAll(JSON.parseObject(ret));
        }
        if (login2Uaa && ((statusCode = registerShadowUser(obj)) == 404)) {
            statusCode = 200;
            addDummyCookie(obj);
        }
        obj.put("regStatusCode", statusCode);
        Utils.GetResponse().setStatus(statusCode);
        return obj.toString();
    }

    public String getOpenId(String code) throws Exception{
        if (Utils.IsCallFromH5()) {
            logger.info("=== isCallFromH5 getOpenId");
            Utils.GetResponse().setStatus(200);
            return "{ \"openid\":\"" + code + "\", \"unionid\":\"" + code + "\"}";
        }
        return wxLogin(code, false, null, null);
    }
    public String login2Uaa(String data) throws Exception{
        JSONObject params = JSON.parseObject(data);
        logger.info("===login2Uaa:{}", data);
        String code = params.getString("code");
        String encryptedData = params.getString("encryptedData");
        String iv = params.getString("iv");
        if (Utils.IsCallFromH5()) {
            logger.info("=== isCallFromH5 login2Uaa");
            JSONObject obj = new JSONObject();
            obj.put("openid", code);
            obj.put("unionid", code);
            obj.put("clientType", "open");
            int statusCode = registerShadowUser(obj);
            if (404 == statusCode) {
                statusCode = 200;
                addDummyCookie(obj);
            }
            Utils.GetResponse().setStatus(statusCode);
            obj.put("regStatusCode", statusCode);
            return obj.toString();
        }
        return wxLogin(code, true, encryptedData, iv);
    }
    private String getUserInfoInternal(String encryptedData, String iv, Map<String,String> map, boolean withCookie) throws Exception{
        encryptedData = java.net.URLEncoder.encode(encryptedData, "UTF-8");
        iv = java.net.URLEncoder.encode(iv, "UTF-8");
        StringBuilder sbUrl = new StringBuilder(128);
        sbUrl.append(Utils.GatewayUrl());
        sbUrl.append("/wxapi/miniapp/decrypt/userInfo?");
        sbUrl.append("encryptedData=").append(encryptedData);
        sbUrl.append("&iv=").append(iv);
        logger.info("===getUserInfo, URL={}", sbUrl.toString());
        JSONObject obj = Utils.Request(sbUrl.toString(), "GET", map, null, withCookie);
        if (200 == obj.getInteger("responseCode"))
            return obj.getJSONObject("responseData").getString("data");
        
        throw new Exception(obj.toString());
    }

    public String getUserInfo(String data) throws Exception{
        JSONObject params = JSON.parseObject(data);
        String encryptedData = params.getString("encryptedData");
        String iv = params.getString("iv");
        return getUserInfoInternal(encryptedData, iv, null, true);
    }

    public void wxCallback(String code, String params) throws Exception{
        params = new String(Base64.getDecoder().decode(params), "UTF-8");
        JSONObject obj = JSON.parseObject(params);
        String type = obj.getString("type");
        String redirect_url = obj.getString("redirect_url");
        logger.info("===type:{}, code:{}, redirect_url:{}", type, code, redirect_url);

        String userInfo = GetInfoFromWX(code, type);
        if (userInfo != null) {
            obj = JSON.parseObject(userInfo);
            obj.put("appId", Utils.GetAppId(type));
            obj.put("clientType", type);
            registerShadowUser(obj);
        } else {
            Utils.GetResponse().setStatus(500);
            return;
        }
        HttpServletResponse response = Utils.GetResponse();
        redirect_url = new String(URLCodec.decodeUrl(redirect_url.getBytes("UTF-8")));
        if (redirect_url.indexOf("?") > 0)
            redirect_url = redirect_url + "&oauth2LoginOk=";
        else
            redirect_url = redirect_url + "?oauth2LoginOk=";
        redirect_url = redirect_url + Base64.getEncoder().encodeToString(obj.toString().getBytes());
        response.setCharacterEncoding("UTF-8"); 
        response.sendRedirect(redirect_url);
    }

    private String GetInfoFromWX(String code, String type) throws Exception {
        StringBuilder sbUrl = new StringBuilder(128);
        sbUrl.append(Utils.GatewayUrl());
        sbUrl.append("/wxapi/mp/oauth2/userInfo?");
        sbUrl.append("code=").append(code);
        sbUrl.append("&type=").append(type);
        sbUrl.append("&lang=zh_CN");
        logger.info("===GetInfoFromWX, URL={}", sbUrl.toString());
        String ret = Utils.GET(sbUrl.toString(), false);
        logger.info("===GetInfoFromWX({})={}", code, ret);
        return ret;
    }

    private void addUserInfo(JSONObject obj, String userInfo) {
        JSONObject ret = JSON.parseObject(userInfo);
        String arr[] = {"user_id", "user_name", "name", "given_name", "family_name", "email", "phone_number", "sub", "previous_logon_time", "groups"};
        for (String item : arr)
            obj.put(item, ret.get(item));
    }

   
    private int registerShadowUser(JSONObject obj) throws Exception {
        JSONObject data = new JSONObject();
        String unionId =  obj.getString("unionid");
        if (unionId == null) unionId =  obj.getString("unionId");
        data.put("unionId", unionId);

        String openId =  obj.getString("openid");
        if (openId == null) openId =  obj.getString("openId");
        data.put("openId",  openId);

        data.put("appId", obj.getString("appId"));
        data.put("appName", Utils.GetRequest().getHeader("X-Tenant-Key"));

        String nickName = obj.getString("nickname");
        if (nickName == null) nickName = obj.getString("nickName");
        data.put("nickname", nickName);

        String avatar = obj.getString("headImgUrl");
        if (avatar == null) avatar = obj.getString("avatarUrl");
        data.put("avatar", avatar);

        data.put("type", obj.getString("clientType"));


        String url = Utils.GatewayUrl() + "/uaa/Users/registerShadowUser";
        logger.info("====registerShadwoUser, data:" + data.toString());

        try {
            ResponseEntity<String> response =  Utils.POST_JSON(url, null, data.toString(), false);
            int statusCode = response.getStatusCodeValue();
            if (statusCode == 200 || statusCode == 201) {
                response = loginInternal(response.getBody());
                if (response != null) {
                    String respStr = response.getBody();

                    List<String> cookies = response.getHeaders().get("Set-Cookie");
                    if (cookies.size() == 0)
                        throw new Exception("Cannot get cookie in login.");

                    if (respStr != null) {
                        addUserInfo(obj, respStr);
                        addSession(cookies.get(0), obj);
                        //addUserJwtCookie(obj);
                    }
                }
            }
            return statusCode;
        } catch (Exception e) {
            logger.error("===Exception catched when registerShadowUser:" + e.getMessage());
            if (e.getMessage().startsWith("404")) {
                return 404;
            }
            throw e;
        }
    }

    private ResponseEntity<String> loginInternal(String userPassword) throws Exception {
        JSONObject ret = JSONObject.parseObject(userPassword);
        MultiValueMap<String, String> postParams = new LinkedMultiValueMap<String, String>();
        postParams.add("username", ret.getString("username"));
        postParams.add("password", ret.getString("password"));

        String url = Utils.GatewayUrl() + "/login";
        logger.info("===login, url:" + url);
        logger.info("=======username:" + ret.getString("username"));
        logger.info("=======password:" + ret.getString("password"));

        return Utils.POST_FORM(url, null, postParams, false);
   }

    private Boolean addSession(String cookie, JSONObject obj) throws Exception {
        JSONObject data = new JSONObject();
        data.put("key",  "openid");
        data.put("value", obj.getString("openid"));

        Map<String,String> map = new HashMap<String,String>();
        map.put("Cookie", cookie);

        String url = Utils.GatewayUrl() + "/session";
        logger.info("===set session, data:" + data.toString());
        ResponseEntity<String> response = Utils.POST_JSON(url, map, data.toString(), true);
        if (response.getStatusCodeValue() == 200 || response.getStatusCodeValue() == 201) {
            List<String> cookies = response.getHeaders().get("Set-Cookie");
            if (cookies.size() == 0)
                throw new Exception("Cannot get cookie in addSession.");
            map.put("Cookie", cookies.get(0));
        }

        data = new JSONObject();
        data.put("key",  "sessionKey");
        data.put("value", obj.getString("session_key"));
        logger.info("===set session, data:" + data.toString());
        response = Utils.POST_JSON(url, map, data.toString(), true);
        if (response.getStatusCodeValue() == 200 || response.getStatusCodeValue() == 201) {
            List<String> cookies = response.getHeaders().get("Set-Cookie");
            if (cookies.size() == 0)
                throw new Exception("Cannot get cookie in addSession.");
            map.put("Cookie", cookies.get(0));
        }
        if (obj.getString("unionid") != null) {
            data = new JSONObject();
            data.put("key",  "unionid");
            data.put("value", obj.getString("unionid"));
            logger.info("===set session, data:" + data.toString());
            response = Utils.POST_JSON(url, map, data.toString(), true);
        }
        if (response.getStatusCodeValue() == 200 || response.getStatusCodeValue() == 201) {
            List<String> cookies = response.getHeaders().get("Set-Cookie");
            if (cookies.size() == 0)
                throw new Exception("Cannot get cookie in addSession.");
            logger.info("===set cookie:" + cookies.get(0));
            Utils.GetResponse().setHeader("Set-Cookie", cookies.get(0));
            return true;
        }
        return false;
    }

    private String getUrl(String code) throws Exception {
        StringBuilder sb = new StringBuilder(256);
        sb.append("https://api.weixin.qq.com/sns/jscode2session?appid=");
        String appId = Utils.GetAppIdFromReferer();
        logger.info("===getUrl, appId:" + appId);
        String url = Utils.GatewayUrl() + "/config/config/configs/search/appid/" + appId;
        String ret = Utils.GET(url, false);
        if (ret != null) {
            JSONArray arr = JSON.parseArray(ret);
            JSONObject config = null, tmp = null;
            for (int i=0, len=arr.size(); i<len; i++) {
                tmp = (JSONObject)arr.get(i);
                if ("com.qq.weixin.miniapp.appSecret".equals(tmp.getString("fkey"))) {
                    config = tmp;
                    break;
                }
            }
            if (config != null) {
                sb.append(appId);
                sb.append("&secret=");
                String secret = config.getString("fvalue");
                if (secret == null || secret.trim().length() == 0)
                    throw new Exception("com.qq.weixin.miniapp.appSecret的值不能为空");
                sb.append(secret.trim());

                sb.append("&js_code=");
                sb.append(code);
                sb.append("&grant_type=authorization_code");
                logger.info("===getUrl:" + sb.toString());
                return sb.toString();
            }
        }
        throw new Exception("未找到APPID=" + appId + "配置信息！");
    }

    private void addUserJwtCookie(JSONObject obj) {
        long expire =  System.currentTimeMillis() / 1000 + 365 * 24 * 60 * 60;
        obj.put("expire", expire);

        String base64Encoded = Base64.getEncoder().encodeToString(obj.toString().getBytes());
        try {
            base64Encoded = java.net.URLEncoder.encode(base64Encoded, "UTF-8");
        } catch (Exception e) {
            logger.error("===addUserJwtCookie, URLEncoder.encode error:" + e.getMessage());
            return;
        }
        Cookie cookie = new Cookie("X-Uaa-JWT", base64Encoded);
        cookie.setMaxAge((int)expire);
        cookie.setPath("/");

        Utils.GetResponse().addCookie(cookie);
    }

    private void addDummyCookie(JSONObject obj) {
        ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest req = sra.getRequest();
        HttpServletResponse res= sra.getResponse();

        String appSecret = req.getHeader("X-Secret");
        if (appSecret == null) {
            logger.error("===addDummyCookie: X-Secret is not set");
            return;
        }
        JSONObject data = new JSONObject();
        String id = obj.getString("unionid");
        if (id == null) {
            id = obj.getString("openid");
        }
        long expire =  System.currentTimeMillis() / 1000 + 365 * 24 * 60 * 60;
        obj.put("user_id", id);
        obj.put("user_name", id);
        data.put("userid", id);
        data.put("username", id);
        data.put("token", id);
        JSONObject info = new JSONObject();
        info.put("openid", id);
        data.put("info", info);
        data.put("expire", expire);
        data.put("hmac", getSignature(appSecret, id + expire));
        logger.info("==addDummyCookie, data:" + data.toString());

        String base64Encoded = Base64.getEncoder().encodeToString(data.toString().getBytes());
        try {
            base64Encoded = java.net.URLEncoder.encode(base64Encoded, "UTF-8");
        } catch (Exception e) {
            logger.error("===addDummyCookie, URLEncoder.encode error:" + e.getMessage());
            return;
        }
        Cookie cookie = new Cookie("user_session", base64Encoded);
        cookie.setMaxAge((int)expire);
        cookie.setHttpOnly(true);
        cookie.setPath("/");

        res.addCookie(cookie);
    }

    private static final String ALGORITHM = "HmacSHA1";

    private static String toHexString(byte[] bytes) {
        Formatter formatter = new Formatter();

        for (byte b : bytes) {
            formatter.format("%02x", b);
        }
        return formatter.toString();
    }

    public static String getSignature(String key, String data) {
        try {
            SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), ALGORITHM);
            Mac mac = Mac.getInstance(ALGORITHM);
            mac.init(keySpec);
            return toHexString(mac.doFinal(data.getBytes()));
        } catch (Exception e) {
            logger.error("===getSignature:" + e.getMessage());
        }
        return "getSignature end.";
    }

}

